OOP I


Uporabnik

Dan je razred

def Uporabnik:
    def init(ime, priimek):
        self.ime = ime
        self.priimek = priimek

    def __str__(self)
        print(ime + " " + priimek)

1. podnaloga

Popravi napake v definiciji razreda. Atributa naj ostaneta ime in priimek.

 >>> jan = Uporabnik('Jan', 'Tisti')
 >>> print(jan)
 Jan Tisti
 >>> jan.priimek
 Tisti
 >>> kitajc = Uporabnik('A', 'Li')
 >>> print(kitajc)
 A Li
 >>> kitajc.ime
 A

Uradna rešitev

class Uporabnik:
    def __init__(self, ime, priimek):
        self.ime = ime
        self.priimek = priimek

    def __str__(self):
        return(self.ime + " " + self.priimek)

2. podnaloga

Spremeni konstruktor tako, da ne bo sprejemal imen in priimkov skupaj krajših od 5 črk.V tem primeru naj bo uporabnik Janez Slovenski.

 >>> jan = Uporabnik('Jan', 'Tisti')
 >>> print(jan)
 Jan Tisti
 >>> jan.priimek
 Tisti
 >>> kitajc = Uporabnik('A', 'Li')
 >>> print(kitajc)
 Janez Slovenski
 >>> kitajc.ime
 Janez

Uradna rešitev

class Uporabnik:
    def __init__(self, ime, priimek):
        if len(ime+priimek) < 5:
            ime = 'Janez'
            priimek = 'Slovenski'
        self.ime = ime
        self.priimek = priimek

    def __str__(self):
        return(self.ime + " " + self.priimek)

3. podnaloga

Dodaj metodo inicialke, ki vrne inicialke uporabnika. Pri tem predpostavi, da ima vsak ime in priimek iz ene besede.

 >>> jan = Uporabnik('Jan', 'Tisti')
 >>> jan.inicialke()
 J.T.
 >>> legenda = Uporabnik('Bruce', 'Lee')
 >>> legenda.inicialke()
 B.L.

Uradna rešitev

class Uporabnik:
    def __init__(self, ime, priimek):
        if len(ime+priimek) < 5:
            ime = 'Janez'
            priimek = 'Slovenski'
        self.ime = ime
        self.priimek = priimek

    def __str__(self):
        return(self.ime + " " + self.priimek)

    def inicialke(self):
        return self.ime[0] + '.' + self.priimek[0] + '.'

4. podnaloga

Ho Ši Minh se je pritožil in rekel, da so njegove inicialke H.Š.M. Podprla ga je tudi Ana Nina Vodičar Mehle, katere inicialke so po njenem A.N.V.M. . Peter Pavlovič Čehov pa je povedal, da se Ana Nina zmišljuje in da so njegove inicialke le P.Č. Da pa je bila zadeva še bolj zaostrena, se je v pogovor vmešal še Sebastian, ki pravi, da so njegove inicialke pač samo S.

Ker jim želiš vsem ustreči, dodaj še metodo polne_inicialke tako, da bo imela parameter polno. Če bo nastavljen na True, so inicialke sestavljene iz začetnic vseh besed, ki sestavljajo ime in priimek. Pri tem štejemo, da ime Ana Marija sestavljata dve besedi, ime Ana-Nuša pa le ena. Privzeta vrednost tega parametra pa je False, kar pomeni, da se vzame le prva črka imena in prva črka priimka (razen za tako pomembne osebe, kot so Sebastian ali pa Sting, ki so pač poznani le po imenu ali priimku - inicialke za oba sta S.).

 >>> jan = Uporabnik('Jan Jankovski', 'Tisti')
 >>> jan.polne_inicialke(True)
 J.J.T.
 >>> jan = Uporabnik('Jan Jankovski', 'Tisti')
 >>> jan.polne_inicialke()
 J.T.
 >>> jan = Uporabnik('Jan Jankovski', 'Tisti')
 >>> jan.polne_inicialke(False)
 J.T.

Uradna rešitev

class Uporabnik:
    def __init__(self, ime, priimek):
        if len(ime + priimek) < 5:
            ime = 'Janez'
            priimek = 'Slovenski'
        self.ime = ime
        self.priimek = priimek

    def __str__(self):
        return (self.ime + " " + self.priimek)

    def polne_inicialke(self, polno = False):
        imena = self.ime.split()
        priimki = self.priimek.split()
        ini = ''
        for im in imena:
            ini += im[0] + '.'
            if not polno: break # le prvo
        for im in priimki:
            ini += im[0] + '.'
            if not polno: break # le prvo
        return ini

Bitni cekini

Trenutno je na spletu zelo popularna digitalna valuta Bitcoin. Osnova za pošteno uporabo take valute so zapleteni kriptografski protokoli, mi pa bomo ubrali malo bolj poenostavljeno različico ter sestavili razred BitniCekin, s katerim bomo predstavili račun nekega lastnika te valute.

POZOR:

Da ne bo težav pri testiranju, pri 2., 3. in 4. podnalogi začnemo z

              class BitniCekini(BitniCekini):

To pomeni, da se v razred "skopirajo" vse definicije, ki ste jih v razred BitniCekini napisali prej. Seveda pa 1. podnalogo še vedno začnemo z

              class BitniCekini():

Druga možnost pa je, da podanloge vedno začnemo z

              class BitniCekini():

a potem v razred vedno napišemo vse metode, ki smo jih definirali v vseh prejšnjih podnalogah!

1. podnaloga

Sestavite razred BitniCekin s konstruktorjem __init__(self, stanje), ki sprejme začetno stanje na računu uporabnika (v valuti Bitcoin). Atribut, v katerega shranite stanje, naj bo poimenovan _stanje.

Argument stanje naj bo neobvezen in v primeru, ko ni podan, naj bo začetno stanje enako nič.

Uradna rešitev

class BitniCekin:
    def __init__(self, stanje=0):
        self._stanje = stanje

2. podnaloga

Sestavite metodo __str__(self), ki predstavi stanje na računu v obliki: 'Število bitnih cekinov na računu: ...'

Primer:

>>> racun = BitniCekin(6)
>>> print(racun)
Število bitnih cekinov na računu: 6

Uradna rešitev

class BitniCekin:
    def __init__(self, stanje=0):
        self._stanje = stanje

    def __str__(self):
        return 'Število bitnih cekinov na računu: {}'.format(self._stanje)

3. podnaloga

Sestavite metodi dvig(self, koliko) in polog(self, koliko), ki dvigneta oz. položita ustrezno količino bitnih cekinov na račun. Predpostavimo, da bo vrednost argumenta koliko vedno nenegativno celo število.

Pri metodi dvig upoštevajte, da stanje na računu ne sme biti negativno. V takšnem primeru se dvig ne sme izvesti.

Metoda dvig naj vrne True, če je dvig uspel in False, če ni. Metoda polog naj vrne stanje na računu po pologu.

Uradna rešitev

class BitniCekin:
    def __init__(self, stanje=0):
        self._stanje = stanje

    def __str__(self):
        return 'Število bitnih cekinov na računu: {}'.format(self._stanje)

    def dvig(self, koliko):
        '''dvig sresdtev in potrditev uspešnosti transakcije'''
        if koliko > self._stanje:
            return False
        else:
            self._stanje -= koliko
            return True

    def polog(self, koliko):
        '''polog sredstev in vrednost trenutnega stanja'''
        self._stanje += koliko
        return self._stanje

4. podnaloga

Sestavite funkcijo prenesi(racun1, racun2, koliko), ki iz računa racun1 prenese koliko cekinov na račun racun2. Funkcija prenesi naj ne bo znotraj razreda BitniCekin, saj ni objektna metoda, ampak je čisto običajna funkcija. Spremenljivki racun1 in racun2 sta seveda objekta tipa BitniCekin, kar ni potrebno preverjati!

Če na računu racun1 ni dovolj denarja, se transakcija ne sme izvršiti, torej mora stanje na obeh računih ostati nespremenjeno. Funkcija naj vrne uspešnost transakcije (True, če je transakcija uspela, in False, če ni).

Uradna rešitev

def prenesi(racun1, racun2, koliko):
    '''prenos iz enega bitnega cekina na drugega in
       indikacija uspešnosti transakcije '''
    if racun1.dvig(koliko):
        racun2.polog(koliko)
        return True
    return False

Datumi

POZOR:

Da ne bo težav pri testiranju, pri 2., 3., 4. in 5. podnalogi začnemo z

              class Datumi(Datumi):

To pomeni, da se v razred "skopirajo" vse definicije, ki ste jih v razred Datumi napisali prej. Seveda pa 1. podnalogo še vedno začnemo z

              class Datumi():

Druga možnost pa je, da podanloge vedno začnemo z

              class Datumi():

a potem v razred vedno napišemo vse metode, ki smo jih definirali v vseh prejšnjih podnalogah!

1. podnaloga

Definirajte razred Datum, s katerim predstavimo datum. Najprej sestavite konstruktor __init__(self, dan, mesec, leto). Atributi razreda Datum naj bodo poimenovani _dan, _mesec in _leto.

>>> d = Datum(21, 5, 2013)
>>> d.leto
2013

Uradna rešitev

class Datum:
    def __init__(self, dan, mesec, leto):
        self._dan = dan
        self._mesec = mesec
        self._leto = leto

2. podnaloga

Razredu Datum dodajte (torej vaš razred mora vsebovati to, kar zahteva 1. naloga) metodo __str__, ki predstavi datum v berljivi obliki 'dan. mesec. leto'.

>>> d = Datum(21, 5, 2013)
>>> print(d)
21. 5. 2013

Uradna rešitev

class Datum:
    def __init__(self, dan, mesec, leto):
        self._dan = dan
        self._mesec = mesec
        self._leto = leto

    def __str__(self):
        return "{0}. {1}. {2}".format(self._dan, self._mesec, self._leto)

3. podnaloga

Razredu Datum dodajte (torej vaš razred mora vsebovati to, kar zahteva 1. naloga) metodo __lt__, ki datum primerja z drugim datumom (metoda naj vrne True, če je prvi datum manjši, in False, če ni).

Ko definirate to metodo, lahko datume primerjate kar z operatorjema < in >. Na primer:

>>> Datum(31, 12, 1999) < Datum(1, 1, 2000)
True

Uradna rešitev

class Datum:
    def __init__(self, dan, mesec, leto):
        self._dan = dan
        self._mesec = mesec
        self._leto = leto

    def __str__(self):
        return "{0}. {1}. {2}".format(self._dan, self._mesec, self._leto)

    def __lt__(self, other):
        # uporabimo Pythonovo vgrajeno leksikografsko primerjavo
        return (self._leto, self._mesec, self._dan) < (other._leto, other._mesec, other._dan)

4. podnaloga

Razredu Datum dodajte (torej vaš razred mora vsebovati to, kar zahteva 1. naloga) metodo __eq__, ki datum primerja z drugim datumom (metoda naj vrne True, če sta datuma enaka, in False, če nista).

Ko definirate to metodo, lahko datume primerjate kar z operatorjema == in !=. Na primer:

>>> Datum(31, 12, 1999) != Datum(1, 1, 2000)
True

Uradna rešitev

class Datum:
    def __init__(self, dan, mesec, leto):
        self._dan = dan
        self._mesec = mesec
        self._leto = leto

    def __eq__(self, other):
        # uporabimo Pythonovo vgrajeno leksikografsko primerjavo
        return (self._leto, self._mesec, self._dan) == (other._leto, other._mesec, other._dan)

5. podnaloga

Razredu Datum dodajte (torej vaš razred mora vsebovati to, kar zahteva 1. naloga) metodo iso8601, ki vrne niz s predstavitvijo datuma oblike yyyy-mm-dd. Na prvem mestu je letnica, nato mesec in na koncu dan. Števila so ločena z znaki '-'. Letnica je štirimestno število, dan in mesec pa dvomestni števili (po potrebi jih dopolnite z vodilnimi ničlami.

>>> Datum(17, 3, 1999).iso8601()
'1999-03-17'

Uradna rešitev

class Datum:
    def __init__(self, dan, mesec, leto):
        self._dan = dan
        self._mesec = mesec
        self._leto = leto

    def iso8601(self):
        return "{0:04}-{1:02}-{2:02}".format(self._leto, self._mesec, self._dan)

Bakterije

1. podnaloga

Definirajte razred Bakterija, s katerim bomo predstavili bakterije. Sestavite konstruktor, ki kot parameter sprejema niz, ki opisuje genski zapis bakterije in ta niz priredi atributu DNA.

Preveriti pa morate, če je dani genski zapis veljaven, se pravi, da vsebuje samo črke 'A','G','C','T' (okrajšave za adenin, gvanin, citozin in timin). Če niz ni veljaven, naj atribut DNA postane prazen niz. Konstruktor mora narediti tudi atribut generacija, ki naj dobi začetno vrednost 0.

Primer:

>>> a = Bakterija('GAAATCGGT')
>>> a.DNA
'GAAATCGGT'
>>> a.generacija
0

Primer z neveljavnim genskim zapisom:

>>> a = Bakterija('ABCDEFGH')
>>> a.DNA
''

Uradna rešitev

class Bakterija:
    def __init__(self, dna):
        self.DNA = dna if all([x in 'ACGT' for x in dna]) else ''
        self.generacija = 0
        # če vam razumevanje zgornje kode dela težave - stvar bi lahko
        # napisali tudi
        #  self.DNA = dna
        #  # ali morda ni veljavno?
        #  for znak in dna:
        #     if znak not in 'ACGT':
        #         self.DNA = '' # napačen znak, zato ...
        #         break
        #  self.generacija = 0

2. podnaloga

Bakterije se zelo rade delijo. Običajne bakterije delijo tako, da iz ene nastaneta dve novi, naše bakterije pa so posplošene bakterije, ki se lahko delijo na poljubno število novih bakterij. Pri tem se njihov genski zapis prekopira, poveča pa se števec generacije.

Definirajte metodo __floordiv__, s katero je definirana operacija // (celoštevilčno deljenje). Metoda sprejema le en parameter (delitelj), ki pove, koliko bakterij dobimo po delitvi. Metoda mora vrniti seznam novih bakterij, ki imajo isti DNA kot začetna bakterija, imajo pa povečan števec generacije. Pri tem pa morate še upoštevati, da se bakterije s praznim genskim zapisom ne morejo deliti. V takšnem primeru naj metoda vrne prazen seznam.

Primer:

>>> a = Bakterija('GAAATCGGT')
>>> nove_bakterije = a//3
>>> len(nove_bakterije)
3
>>> print(nove_bakterije)
[<__main__.Bakterija object at 0x7ffd41edac50>, <__main__.Bakterija object at 0x7ffd41edab90>, <__main__.Bakterija object at 0x7ffd41edab50>]
>>> nove_bakterije[0].generacija
1
>>> nove_bakterije[0].DNA
'GAAATCGGT'

Uradna rešitev

class Bakterija(Bakterija):
    # zgornji zapis pomeni, da "poberemo" vse, kar smo do sedaj
    # že napisali v razredu Bakterija
    def __floordiv__(self, n):
        if not self.DNA:
            return []
        nove = []
        for i in range(n):
            b = Bakterija(self.DNA)
            b.generacija = self.generacija + 1
            nove.append(b)
        return nove

3. podnaloga

Bakterije se lahko tudi združujejo. Pri tem nastane nova bakterija, katere genski zapis je kombinacija genskih zapisov obeh bakterij, ki nastopata v združevanju. Zapis se združuje po takšnem pravilu: izmenično se jemlje po eno črko iz obeh genskih zapisov, ko pa pridemo do konca krajšega genskega zapisa, se preostanek daljšega doda na konec novega zapisa. Tako bi na primer pri združevanju zapisov ACT in GCTATGCCC dobili AGCCTTATGCCC.

Definirajte metodo __add__, s katero je definirana operacija + (seštevanje). To pomeni, da bomo bakterije lahko združevali kar z uporabo operatorja +. Metoda sprejema en parameter: drugo bakterijo, ki jo bomo združili s prvo. Metoda naj vrne novo bakterijo, ki ima združen genski zapis ter za ena večji števec generacije kot starejša od obeh bakterij, ki nastopata v združevanju.

Primer:

>>> a = Bakterija('ACT')
>>> b = Bakterija('GCTATGCCC')
>>> c = a + b
>>> c.DNA
'AGCCTTATGCCC'
>>> c.generacija
1

Uradna rešitev

class Bakterija(Bakterija):
    # zgornji zapis pomeni, da "poberemo" vse, kar smo do sedaj
    # že napisali v razredu Bakterija
    def __add__(self, other):
        dna = ''
        ml = min(len(self.DNA), len(other.DNA))
        for i in range(ml):
            dna += self.DNA[i] + other.DNA[i]
        dna += self.DNA[ml:] + other.DNA[ml:]
        nova = Bakterija(dna)
        nova.generacija = 1 + max(self.generacija, other.generacija)
        return nova

4. podnaloga

Bakterije lahko tudi mutirajo, pri čemer se jim spremeni genski zapis. Na primer, podzaporedje AAG se pri mutaciji spremeni v podzaporedje AAT. Pri mutaciji se vedno spremenijo vse pojavitve danega podzaporedja, če pa genski zapis bakterije takšnega podzaporedja ne vsebuje, pa se med mutacijo DNA seveda ne spremeni.

Napišite metodo mutacija, ki sprejme dva parametra, ki povesta, v kaj se pri mutaciji spremeni dano podzaporedje genskega zapisa. Metoda naj vrne novo bakterijo, ki ima mutiran genski zapis in za ena večji števec generacije. Upoštevajte, da se mutacije genskega zapisa vedno odvijajo v smeri od leve proti desni (običajna smer v katero naraščajo indeksi).

Upoštevajte tudi, da bakterije brez genskega zapisa ne morejo mutirati. V takšnem primeru naj metoda vrne isto, nespremenjeno bakterijo (nasvet: self).

Nasvet: pomagajte si z metodo replace!

Primer:

>>> a = Bakterija('GAAATCGGT')
>>> m = a.mutacija('AAT', 'CAT')
>>> m.DNA
'GACATCGGT'
>>> m.generacija
1

Uradna rešitev

class Bakterija(Bakterija):
    # zgornji zapis pomeni, da "poberemo" vse, kar smo do sedaj
    # že napisali v razredu Bakterija
    def mutacija(self, fr, to):
        if not self.DNA:
            return self

        MDNA = self.DNA.replace(fr, to)
        nova = Bakterija(MDNA)
        nova.generacija = self.generacija + 1
        return nova

Mnogokotnik

Mnogokotnik v ravnini lahko predstavimo s seznamom njegovih oglišč (pari števil). Na primer seznam

[(0, 0), (1, 0), (1, 1), (0, 1)]

opisuje kvadrat, seznam

[(2, 0), (2, 1), (0, 2), (-2, 1), (-2, 0), (0, 1)]

pa opisuje (nekonveksen) šestkotnik.

1. podnaloga

Sestavite razred Mnogokotnik, s katerim predstavimo ravninski mnogokotnik. Najprej sestavite konstruktor __init__(self, ogl), kjer je ogl seznam njegovih oglišč. Atribut razreda naj bo poimenovan oglisca.

Nekateri ljudje mnogokotnike zapisujejo tako, da na konec seznama oglišč ponovno postavijo prvo oglišče (s tem poudarijo, da je mnogokotnik sklenjen). Zgornji kvadrat bi torej zapisali takole:

[(0, 0), (1, 0), (1, 1), (0, 1), (0, 0)]

V našem razredu Mnogokotnik naj se dolžina seznama oglisca ujema s številom oglišč mnogokotnika. Podvojeno oglišče na koncu seznama naj konstruktor po potrebi odstrani. Zgled:

>>> p = Mnogokotnik([(0, 0), (1, 0), (1, 1), (0, 1), (0, 0)])
>>> p.oglisca
[(0, 0), (1, 0), (1, 1), (0, 1)]

Predpostavite lahko, da mnogokotniki ne bodo izrojeni (tj. če imata dve stranici neprazen presek, sta nujno sosednji, presek pa je natanko njuno skupno oglišče).

Uradna rešitev

class Mnogokotnik(object):
    def __init__(self, oglisca):
        self.oglisca = list(oglisca)
        if self.oglisca[0] == self.oglisca[-1]:
            self.oglisca.pop()

2. podnaloga

V razredu Mnogokotnik definirajte metodo obseg(self), ki izračuna in vrne njegov obseg. Zgled:

>>> p = Mnogokotnik([(0, 0), (1, 0), (1, 1), (0, 1), (0, 0)])
>>> p.obseg()
4.0

Uradna rešitev

def dolzina_daljice(a, b):
    ax, ay = a
    bx, by = b
    return ((ax - bx) ** 2 + (ay - by) ** 2) ** 0.5


class Mnogokotnik(Mnogokotnik):
    def obseg(self):
        ob = 0
        n = len(self.oglisca)
        for i in range(n):
            ob += dolzina_daljice(self.oglisca[i], self.oglisca[(i + 1) % n])
        return ob

3. podnaloga

V razredu Mnogokotnik definirajte metodo ploscina(self), ki izračuna in vrne njegovo ploščino.

Ploščina mnogokotnika (brez samopresečišč) z oglišči $(x_1, y_1), (x_2, y_2), \ldots, (x_n, y_n)$ je $|D|/2$, kjer je $D = x_1 y_2 - x_2 y_1 + x_2 y_3 - x_3 y_2 + \ldots + x_{n-1} y_n - x_n y_{n-1} + x_n y_1 - x_1 y_n$.

>>> p = Mnogokotnik([(0, 0), (1, 0), (1, 1), (0, 1)])
>>> p.ploscina()
1.0

Uradna rešitev

class Mnogokotnik(Mnogokotnik):
    def ploscina(self):
        a = 0
        n = len(self.oglisca)
        for i in range(n):
            x, y = self.oglisca[i]
            x2, y2 = self.oglisca[(i + 1) % n]
            a += x * y2 - x2 * y
        return abs(a) / 2

4. podnaloga

V razredu Mnogokotnik definirajte metodo je_konveksen(self), ki vrne True, če je mnogokotnik konveksen in False sicer.

Predstavljajte si, da je ravnina, na kateri leži mnogokotnik, vložena v trirazsežni prostor tako, da je $z = 0$. Na bodo $P$, $Q$ in $R$ tri zaporedna oglišča mnogokotnika. Naj bo $u$ vektor od $P$ do $Q$ in naj bo $v$ vektor od $Q$ do $R$. Potem je $z$-komponenta vektorskega produkta vektorjev $u$ in $v$ pozitivna, če $PQR$ "zavije v levo", in negativna sicer. Mnogokotnik je konveksen, če vedno "zavijamo v isto smer", ko delamo obhod po robu mnogokotnika. Zgled:

>>> p = Mnogokotnik([(0, 0), (1, 0), (1, 1), (0, 1)])
>>> p.je_konveksen()
True
>>> q = Mnogokotnik([(2, 0), (2, 1), (0, 2), (-2, 1), (-2, 0), (0, 1)])
>>> q.je_konveksen()
False

Predpostavite lahko, da nobeni dve zaporedni stranici ne oklepata iztegnjenega kota (180 °).

Uradna rešitev

def vektorski_produkt(a, b):
    ax, ay, az = a
    bx, by, bz = b
    return (ay * bz - az * by, az * bx - ax * bz, ax * by - ay * bx)

def smer_vrtenja(u, v, w):
    ux, uy = u
    vx, vy = v
    wx, wy = w
    z = vektorski_produkt((vx - ux, vy - uy, 0), (wx - vx, wy - vy, 0))[2]
    return 1 if z > 0 else -1

class Mnogokotnik(Mnogokotnik):
    def je_konveksen(self):
        n = len(self.oglisca)
        znak = smer_vrtenja(self.oglisca[0], self.oglisca[1], self.oglisca[2])
        for i in range(1, n):
            u = self.oglisca[i % n]
            v = self.oglisca[(i + 1) % n]
            w = self.oglisca[(i + 2) % n]
            znak2 = smer_vrtenja(u, v, w)
            if znak2 * znak < 0:
                return False
        return True